package org.streets.wicket.internal;

import org.apache.wicket.util.file.File;
import org.apache.wicket.util.listener.IChangeListener;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.watch.ModificationWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern;

/**
 * 
 * @author dzb
 */
public final class ReloadingClassLoader extends URLClassLoader
{
    private static final Logger logger = LoggerFactory.getLogger(ReloadingClassLoader.class);

    private final Duration pollFrequency = Duration.seconds(3);
    private final List<URL> urls = new ArrayList<URL>();
    private final ModificationWatcher watcher;
    private final Pattern[] patterns;

    private IChangeListener listener;

    public ReloadingClassLoader()
    {
        this(ReloadingClassLoader.class.getClassLoader(), ".*");
    }

    public ReloadingClassLoader(String... patterns)
    {
        this(ReloadingClassLoader.class.getClassLoader(), patterns);
    }

    public ReloadingClassLoader(ClassLoader parent, String... patterns)
    {
        super(new URL[] {}, parent);
        this.patterns = buildPatterns(patterns);
        addURLs(ReloadingClassLoader.class.getClassLoader());
        addURLs(parent);
        watcher = new ModificationWatcher(pollFrequency);

        if(logger.isTraceEnabled() || logger.isDebugEnabled())
        {
            logger.debug("Creating new ReloadingClassLoader. Packages to reload:");
            for(Pattern pattern : this.patterns)
            {
                logger.debug("    - {}", pattern);
            }
            logger.trace("Reloadable classpath:");
            for(URL url : urls)
            {
                logger.trace("    - {}", url);
            }
        }
    }

    private Pattern[] buildPatterns(String... patterns)
    {
        if(patterns != null && patterns.length > 0)
        {
            Pattern[] p = new Pattern[patterns.length];
            for(int i = 0; i < patterns.length; i++)
            {
                p[i] = Pattern.compile(patterns[i]);
            }
            return p;
        }
        return new Pattern[0];
    }

    private void addURLs(ClassLoader cl)
    {
        if(cl != null)
        {
            try
            {
                final Enumeration<URL> resources = cl.getResources("");
                while(resources.hasMoreElements())
                {
                    addURL(resources.nextElement());
                }
            }
            catch(IOException e)
            {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    protected void addURL(URL url)
    {
        if(!urls.contains(url))
        {
            urls.add(url);
            super.addURL(url);
        }
    }

    @Override
    public URL getResource(final String name)
    {
        URL resource = findResource(name);
        ClassLoader parent = getParent();
        if(resource != null)
        {
            return resource;
        }
        return parent.getResource(name);
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        Class<?> clazz = findLoadedClass(name);
        if(clazz == null)
        {
            final ClassLoader parent = getParent();
            if(tryClassHere(name))
            {
                try
                {
                    clazz = findClass(name);
                    watchForModifications(clazz);
                }
                catch(ClassNotFoundException cnfe)
                {
                    if(parent == null)
                    {
                        throw cnfe;
                    }
                }
            }
            if(clazz == null)
            {
                if(parent == null)
                {
                    throw new ClassNotFoundException(name);
                }
                else
                {
                    clazz = parent.loadClass(name);
                }
            }
        }
        if(resolve)
        {
            resolveClass(clazz);
        }
        return clazz;
    }

    private boolean tryClassHere(String name)
    {
        if(name != null && (name.startsWith("java.") || name.startsWith("javax.servlet")))
        {
            return false;
        }
        if(patterns.length == 0)
        {
            return true;
        }
        for(Pattern pattern : patterns)
        {
            if(pattern.matcher(name).matches())
            {
                return true;
            }
        }
        return false;
    }

    private void watchForModifications(Class<?> clz)
    {
        for(URL location : urls)
        {
            String clzLocation = location.getFile() + clz.getName().replaceAll("\\.", "/") + ".class";
            final File clzFile = new File(clzLocation);
            if(clzFile.exists())
            {
                logger.trace("Watching changes of class '{}'", clzFile);
                watcher.add(clzFile, new IChangeListener()
                {
                    public void onChange()
                    {
                        logger.info("Reloading changed class file: {}", clzFile);
                        try
                        {
                            listener.onChange();
                        }
                        catch(Exception e)
                        {
                            logger.error("Error calling lister on change of class file: " + clzFile, e);
                            watcher.remove(clzFile);
                        }
                    }
                });
                break;
            }
        }
    }

    public void setListener(IChangeListener listener)
    {
        this.listener = listener;
    }

    public void destroy()
    {
        watcher.destroy();
    }
}